#include <xrpl/basics/base_uint.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/NFTokenID.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/jss.h>

#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <vector>

namespace ripple {

bool
canHaveNFTokenID(
    std::shared_ptr<STTx const> const& serializedTx,
    TxMeta const& transactionMeta)
{
    if (!serializedTx)
        return false;

    TxType const tt = serializedTx->getTxnType();
    if (tt != ttNFTOKEN_MINT && tt != ttNFTOKEN_ACCEPT_OFFER &&
        tt != ttNFTOKEN_CANCEL_OFFER)
        return false;

    // if the transaction failed nothing could have been delivered.
    if (transactionMeta.getResultTER() != tesSUCCESS)
        return false;

    return true;
}

std::optional<uint256>
getNFTokenIDFromPage(TxMeta const& transactionMeta)
{
    // The metadata does not make it obvious which NFT was added.  To figure
    // that out we gather up all of the previous NFT IDs and all of the final
    // NFT IDs and compare them to find what changed.
    std::vector<uint256> prevIDs;
    std::vector<uint256> finalIDs;

    for (STObject const& node : transactionMeta.getNodes())
    {
        if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_PAGE)
            continue;

        SField const& fName = node.getFName();
        if (fName == sfCreatedNode)
        {
            STArray const& toAddPrevNFTs = node.peekAtField(sfNewFields)
                                               .downcast<STObject>()
                                               .getFieldArray(sfNFTokens);
            std::transform(
                toAddPrevNFTs.begin(),
                toAddPrevNFTs.end(),
                std::back_inserter(finalIDs),
                [](STObject const& nft) {
                    return nft.getFieldH256(sfNFTokenID);
                });
        }
        else if (fName == sfModifiedNode)
        {
            // When a mint results in splitting an existing page,
            // it results in a created page and a modified node. Sometimes,
            // the created node needs to be linked to a third page, resulting
            // in modifying that third page's PreviousPageMin or NextPageMin
            // field changing, but no NFTs within that page changing. In this
            // case, there will be no previous NFTs and we need to skip.
            // However, there will always be NFTs listed in the final fields,
            // as rippled outputs all fields in final fields even if they were
            // not changed.
            STObject const& previousFields =
                node.peekAtField(sfPreviousFields).downcast<STObject>();
            if (!previousFields.isFieldPresent(sfNFTokens))
                continue;

            STArray const& toAddPrevNFTs =
                previousFields.getFieldArray(sfNFTokens);
            std::transform(
                toAddPrevNFTs.begin(),
                toAddPrevNFTs.end(),
                std::back_inserter(prevIDs),
                [](STObject const& nft) {
                    return nft.getFieldH256(sfNFTokenID);
                });

            STArray const& toAddFinalNFTs = node.peekAtField(sfFinalFields)
                                                .downcast<STObject>()
                                                .getFieldArray(sfNFTokens);
            std::transform(
                toAddFinalNFTs.begin(),
                toAddFinalNFTs.end(),
                std::back_inserter(finalIDs),
                [](STObject const& nft) {
                    return nft.getFieldH256(sfNFTokenID);
                });
        }
    }

    // We expect NFTs to be added one at a time.  So finalIDs should be one
    // longer than prevIDs.  If that's not the case something is messed up.
    if (finalIDs.size() != prevIDs.size() + 1)
        return std::nullopt;

    // Find the first NFT ID that doesn't match.  We're looking for an
    // added NFT, so the one we want will be the mismatch in finalIDs.
    auto const diff = std::mismatch(
        finalIDs.begin(), finalIDs.end(), prevIDs.begin(), prevIDs.end());

    // There should always be a difference so the returned finalIDs
    // iterator should never be end().  But better safe than sorry.
    if (diff.first == finalIDs.end())
        return std::nullopt;

    return *diff.first;
}

std::vector<uint256>
getNFTokenIDFromDeletedOffer(TxMeta const& transactionMeta)
{
    std::vector<uint256> tokenIDResult;
    for (STObject const& node : transactionMeta.getNodes())
    {
        if (node.getFieldU16(sfLedgerEntryType) != ltNFTOKEN_OFFER ||
            node.getFName() != sfDeletedNode)
            continue;

        auto const& toAddNFT = node.peekAtField(sfFinalFields)
                                   .downcast<STObject>()
                                   .getFieldH256(sfNFTokenID);
        tokenIDResult.push_back(toAddNFT);
    }

    // Deduplicate the NFT IDs because multiple offers could affect the same NFT
    // and hence we would get duplicate NFT IDs
    sort(tokenIDResult.begin(), tokenIDResult.end());
    tokenIDResult.erase(
        unique(tokenIDResult.begin(), tokenIDResult.end()),
        tokenIDResult.end());
    return tokenIDResult;
}

void
insertNFTokenID(
    Json::Value& response,
    std::shared_ptr<STTx const> const& transaction,
    TxMeta const& transactionMeta)
{
    if (!canHaveNFTokenID(transaction, transactionMeta))
        return;

    // We extract the NFTokenID from metadata by comparing affected nodes
    if (auto const type = transaction->getTxnType(); type == ttNFTOKEN_MINT)
    {
        std::optional<uint256> result = getNFTokenIDFromPage(transactionMeta);
        if (result.has_value())
            response[jss::nftoken_id] = to_string(result.value());
    }
    else if (type == ttNFTOKEN_ACCEPT_OFFER)
    {
        std::vector<uint256> result =
            getNFTokenIDFromDeletedOffer(transactionMeta);

        if (result.size() > 0)
            response[jss::nftoken_id] = to_string(result.front());
    }
    else if (type == ttNFTOKEN_CANCEL_OFFER)
    {
        std::vector<uint256> result =
            getNFTokenIDFromDeletedOffer(transactionMeta);

        response[jss::nftoken_ids] = Json::Value(Json::arrayValue);
        for (auto const& nftID : result)
            response[jss::nftoken_ids].append(to_string(nftID));
    }
}

}  // namespace ripple
